/*
 * Copyright (c) 2009, Hamish Morgan. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the University of Sussex nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package locusts.server;

import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import locusts.common.Constants;
import locusts.common.Player;
import locusts.common.World;
import locusts.common.entities.Actor;
import locusts.common.entities.CollidableEntity;
import locusts.common.entities.CropsEntity;
import locusts.common.entities.Entity;
import locusts.common.entities.EntityData;
import locusts.common.entities.EntityType;
import locusts.common.entities.EntityTypeList;
import locusts.common.entities.InputEntity;
import locusts.common.net.InputMessage;
import locusts.lib.MathUtil;
import locusts.server.EventManager.AbsolutePosition;
import locusts.server.EventManager.EventTask;
import locusts.server.EventManager.ListedPosition;
import locusts.server.EventManager.Position;
import locusts.server.EventManager.VariablePosition;
import locusts.server.behaviours.Average;
import locusts.server.behaviours.AvoidEntitiesFF;
import locusts.server.behaviours.Behaviour;
import locusts.server.behaviours.BehaviourList;
import locusts.server.behaviours.BoundryAvoid;
import locusts.server.behaviours.FlockAlignment;
import locusts.server.behaviours.FlockCohesion;
import locusts.server.behaviours.FlockSeparation;
import locusts.server.behaviours.SeekMaxEnergy;
import locusts.server.behaviours.Wander;
import locusts.server.collisions.AttachCR;
import locusts.server.collisions.CollisionSystem;
import locusts.server.collisions.DamageLocustCR;
import locusts.server.collisions.EatCropsCR;
import locusts.server.collisions.GrowShrinkCropPile;
import locusts.server.collisions.ReflectRC;
import locusts.server.collisions.SpawnCropPile;
import locusts.server.collisions.ReplenishEnergyCR;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Collections of static methods that allow a game to be constructed.
 * 
 * @author Hamish Morgan
 */
public class GameFactory {

    public static final Logger LOG =
            LoggerFactory.getLogger(GameFactory.class);
    private static final Random RAND = new Random();

    private GameFactory() {
    }

    public static Game makeGame() {

        final World world = new World(800, 600);
        final Game game = new Game(world);
        CollisionSystem cs = game.getCollisionSystem();

        EntityType locust = new EntityType(Constants.LOCUST);
        EntityType deadLocust = new EntityType(Constants.DEAD_LOCUST);
        EntityType wall = new EntityType(Constants.WALL);
        EntityType farm = new EntityType(Constants.FARM);
        EntityType crops = new EntityType(Constants.CROPS);
        EntityType mouseDown = new EntityType(Constants.MOUSE_DOWN);
        EntityType mouseMove = new EntityType(Constants.MOUSE_MOVE);

        int locustId = locust.getId();
        int deadLocustId = deadLocust.getId();
        int wallId = wall.getId();
        int farmId = farm.getId();
        int cropsId = crops.getId();
        int mouseDownId = mouseDown.getId();
        int mouseMoveId = mouseMove.getId();

        game.getEntityTypes().addAll(locust, deadLocust, wall, crops, farm,
                mouseDown, mouseMove);

        EntityData locustData = new EntityData();
        locustData.setShape(new Ellipse2D.Double(-10, -10, 20, 20));
        locustData.setDieOnMinEnergy(true);
        locustData.setMinEnergy(1);
        locustData.setMaxEnergy(100);
        cs.addResponce(locustId, mouseMoveId, new ReflectRC());
        cs.addResponce(locustId, mouseDownId, new DamageLocustCR());
        cs.addResponce(locustId, locustId, new ReflectRC());
        cs.addResponce(locustId, wallId, new ReflectRC());
        cs.addResponce(locustId, farmId, new EatCropsCR(game.getEntityData()));
        cs.addResponce(locustId, cropsId, new EatCropsCR(game.getEntityData()));

        EntityData wallData = new EntityData();
        wallData.setShape(new Rectangle2D.Double(-100, -20, 200, 40));
        wallData.setDieOnMinEnergy(false);
        cs.addResponce(wallId, mouseDownId, new AttachCR(game));
        wallData.setEnergyDepletionRate(0);

        EntityData farmData = new EntityData();
        farmData.setShape(new Rectangle2D.Double(-100, -50, 200, 100));
        farmData.setDieOnMinEnergy(false);
        farmData.setMaxEnergy(1000);
        farmData.setEnergyDepletionRate(0);
        cs.addResponce(farmId, mouseDownId, new ReplenishEnergyCR(game.
                getEntityData()));
        cs.addResponce(farmId, mouseDownId, new SpawnCropPile(game));

        EntityData cropsData = new EntityData();
        cropsData.setMaxEnergy(200);
        cropsData.setMinEnergy(1);
        cropsData.setDieOnMinEnergy(true);
        cropsData.setEnergyDepletionRate(0);
        cs.addResponce(cropsId, mouseDownId, new AttachCR(game));
        cs.addResponce(cropsId, farmId, new GrowShrinkCropPile(game.
                getEntityData()));

        EntityData mouseDownData = new EntityData();
        mouseDownData.setEnergyDepletionRate(0);
        mouseDownData.setShape(new Ellipse2D.Double(-4, -4, 8, 8));
//        mouseDownData.setShape(new Ellipse2D.Double(-200, -200, 400, 400));

        EntityData mouseMoveData = new EntityData();
        mouseMoveData.setEnergyDepletionRate(0);
        mouseMoveData.setShape(mouseDownData.getShape());

        EntityData deadLocustData = new EntityData();
        deadLocustData.setShape(locustData.getShape());
        deadLocustData.setDieOnMinEnergy(true);
        deadLocustData.setMinEnergy(1);
        deadLocustData.setMaxEnergy(locustData.getMaxEnergy());
        cs.addResponce(deadLocustId, mouseDownId, new AttachCR(game));

        game.getEntityData().put(locust, locustData);
        game.getEntityData().put(wall, wallData);
        game.getEntityData().put(farm, farmData);
        game.getEntityData().put(crops, cropsData);
        game.getEntityData().put(mouseDown, mouseDownData);
        game.getEntityData().put(mouseMove, mouseMoveData);
        game.getEntityData().put(deadLocust, deadLocustData);

        //
        // ===============================================================
        // Behaviours
        //
//
        locustData.setBehaviour(makeLocustBehaviour(game.getEntityTypes(),
                world.getBounds()));
        //
        // ===============================================================
        // Spawn Entities
        //
//        spawnEntities(game);

        return game;
    }

    public static void load(Game game, Props props) {
//        if (props.containsKey("startEntities")) {
//            String[] startEnts = props.getProperty("startEntities").split(",");
//            for (String key : startEnts) {
//                key = key.trim();
//                loadEntities(game, props, key);
//            }
//        }
        if (props.containsKey("events")) {
            List<EventTask> events = getEvents(props, game);
            game.getEventManager().schedule(events);
        }
    }

//    public static int loadPos(String pos) {
//        String[] parts = pos.split("\\|");
//        int result = 0;
//        for (String part : parts) {
//            part = part.trim().toUpperCase();
//            if (part.length() == 0)
//                continue;
//            try {
//                int tmp = GameFactory.class.getField(part).getInt(null);
//                result |= tmp;
//            } catch (Throwable ex) {
//                LOG.error("Unknown positional value: " + part, ex);
//            }
//        }
//        return result;
//    }
//
//    public static void loadEntities(Game game, Properties props, String key) {
//        String default_pos = "XRANDOM | YRANDOM | ORANDOM";
//        if (!props.containsKey("entities." + key))
//            return;
//        final String type = props.getProperty("entities." + key + ".type");
//
//        final int num = Integer.parseInt(props.getProperty("entities." + key +
//                ".num", "1"));
//
//        final int dpos = loadPos(props.getProperty("entities." + key + ".pos",
//                default_pos));
//
//
//        final int[] pos = new int[num];
//        for (int i = 0; i < num; i++) {
//            pos[i] = loadPos(props.getProperty("entities." + key + "[" + i +
//                    "].pos", default_pos));
//        }
//
//        spawnEntities(game, type, num, dpos, pos);
//
//    }

    public static void loadGameProperties(Game game) {

        final Props props = new Props();
        try {
            props.load(new FileInputStream("res/game.properties"));
            load(game, props);
        } catch (FileNotFoundException ex) {
            LOG.error(null, ex);
        } catch (IOException ex) {
            LOG.error(null, ex);
        }
    }

    public static void spawnEntities(Game game, String type, int num,
            int defaultPos, int[] pos) {
        for (int i = 0; i < num; i++) {
            Entity locust = makeEntity(game, type);
            if (pos.length > i)
                setPosition(game, locust, pos[i]);
            else
                setPosition(game, locust, defaultPos);
            game.addEntity(locust);
        }
    }
    public static final int TOP = 0x01;
    public static final int MIDDLE = 0x02;
    public static final int BOTTOM = 0x04;
    public static final int YRANDOM = 0x08;
    public static final int LEFT = 0x10;
    public static final int CENTER = 0x20;
    public static final int RIGHT = 0x40;
    public static final int XRANDOM = 0x80;
    public static final int NORTH = 0x0100;
    public static final int SOUTH = 0x0200;
    public static final int EAST = 0x0400;
    public static final int WEST = 0x0800;
    public static final int ORANDOM = 0x1000;

    public static void setPosition(Game game, Entity entity, int pos) {
        if ((pos & TOP) != 0)
            entity.setY(0);
        else if ((pos & MIDDLE) != 0)
            entity.setY(game.getWorld().getHeight() / 2);
        else if ((pos & BOTTOM) != 0)
            entity.setY(game.getWorld().getHeight());
        else if ((pos & YRANDOM) != 0)
            entity.setY(RAND.nextDouble() * game.getWorld().getHeight());
        if ((pos & LEFT) != 0)
            entity.setX(0);
        else if ((pos & CENTER) != 0)
            entity.setX(game.getWorld().getWidth() / 2);
        else if ((pos & RIGHT) != 0)
            entity.setX(game.getWorld().getWidth());
        else if ((pos & XRANDOM) != 0)
            entity.setX(RAND.nextDouble() * game.getWorld().getWidth());
        if ((pos & NORTH) != 0)
            entity.setOrientation(0);
        else if ((pos & SOUTH) != 0)
            entity.setOrientation(Math.PI);
        else if ((pos & EAST) != 0)
            entity.setOrientation(Math.PI / 2d);
        else if ((pos & WEST) != 0)
            entity.setOrientation(-Math.PI / 2d);
        else if ((pos & ORANDOM) != 0)
            entity.setOrientation(RAND.nextDouble() * Math.PI * 2);
    }

    public static Entity makeEntity(Game game, String type) {
        if (Constants.CROPS.equals(type)) {
            return makeCropsPile(game);
        } else if (Constants.FARM.equals(type)) {
            return makeFarm(game);
        } else if (Constants.LOCUST.equals(type)) {
            return makeLocust(game);
        } else if (Constants.DEAD_LOCUST.equals(type)) {
            return makeDeadLocust(game, null);
        } else if (Constants.MOUSE_DOWN.equals(type)) {
            return null;
        } else if (Constants.MOUSE_MOVE.equals(type)) {
            return null;
        } else if (Constants.WALL.equals(type)) {
            return makeWall(game);
        }
        return null;
    }

    public static Entity makeEntity(Game game, EntityType type) {
        return makeEntity(game, type.getName());
    }

    public static Entity makeFarm(Game game) {
        int farmId = game.getEntityTypes().getName(Constants.FARM).getId();
        Entity farm = new Entity(farmId, 0, 0, 0);
        farm.setzOrder(Constants.FARM_ZORDER);
        return farm;
    }

    public static Entity makeWall(Game game) {
        int id = game.getEntityTypes().getName(Constants.WALL).getId();
        Entity p = new Entity(id, 0, 0, 0);//x, y, heading);
        p.setzOrder(Constants.WALL_ZORDER);
        return p;
    }

    public static Actor makeLocust(Game game) {
        int id = game.getEntityTypes().getName(Constants.LOCUST).getId();
        final double heading = (RAND.nextDouble() * MathUtil.TWO_PI);
        final double vx = (Math.cos(heading) * 1d);
        final double vy = (Math.sin(heading) * 1d);
        Actor a = new Actor(id, 0, 0, vx, vy, 0, 0, heading);
        a.setzOrder(Constants.LOCUST_ZORDER);
        return a;
    }

    public static Entity makeDeadLocust(Game game) {
        int id = game.getEntityTypes().getName(Constants.DEAD_LOCUST).getId();
        final Entity deadLocust = new Entity(id, 0, 0, 0);
        deadLocust.setzOrder(Constants.DEAD_LOCUST_ZORDER);
        return deadLocust;
    }

    public static Entity makeDeadLocust(Game game, Entity aliveLocust) {
        final Entity deadLocust = makeDeadLocust(game);
        deadLocust.setPosition(aliveLocust.getX(), aliveLocust.getY());
        deadLocust.setOrientation(aliveLocust.getOrientation());
        return deadLocust;
    }

    public static Entity makeInputDown(Game game, InputMessage msg, Player player) {
        EntityType type = game.getEntityTypes().getName(Constants.MOUSE_DOWN);
        EntityData data = game.getEntityData().get(type);
        int id = game.getEntityTypes().getName(Constants.MOUSE_DOWN).getId();
        final Entity press;
        if (msg instanceof InputMessage.Touch) {
            press = new InputEntity(id, player,
                    msg.getX(), msg.getY(),
                    ((InputMessage.Touch) msg).getRect());
        } else {
            press = new InputEntity(
                    id, player, msg.getX(), msg.getY(),
                    data.getShape());
        }
        press.setzOrder(Constants.MOUSE_DOWN_ZORDER);
        return press;
    }

    public static void makeInputDrag(Game game, Entity press) {
        final int dragId = game.getEntityTypes().getName(
                Constants.MOUSE_MOVE).getId();
        press.setTypeId(dragId);
        press.setzOrder(Constants.MOUSE_MOVE_ZORDER);
        press.setModified();
    }

    public static void updateInputEntity(Entity drag, InputMessage msg) {
        drag.setX(msg.getX());
        drag.setY(msg.getY());
        if (msg instanceof InputMessage.Touch) {
            ((CollidableEntity) drag).setShape(
                    ((InputMessage.Touch) msg).getRect());
        }
        drag.setModified();
    }

    public static CropsEntity makeCropsPile(Game game) {
        final int typeId = game.getEntityTypes().getName(Constants.CROPS).
                getId();
        final CropsEntity crops = new CropsEntity(
                typeId, 0, 0, 1d);
        crops.setzOrder(Constants.CROPS_ZORDER);
        crops.setEnergy(2);
        return crops;
    }

    public static Behaviour makeLocustBehaviour(EntityTypeList types,
            Rectangle2D worldBounds) {

        final double maxDistance = 100d;
        final double minDistance = 0d;
        final double maxAngle = 1d * Math.PI;
        //
        final double alignmentWeight = 1d;
        //
        final double cohesionWeight = 1d;
        final double cohesionMaxDistance = 200d;
        //
        final double separationWeight = 1d;
        final double seperationMaxDistance = 50d;
        //
        final double wanderWeight = 0.1d;
        final double boundryWeight = 1d;
        final double seekWeight = 4d;
        final double seekMaxDistance = Double.POSITIVE_INFINITY;

        final double objectAvoidWeight = 2d;
        final double objectAvoidForce = 30;

        BehaviourList behaviours = new Average(10);
        behaviours.add(new FlockAlignment(alignmentWeight, types.getName(
                Constants.LOCUST),
                minDistance, maxDistance, maxAngle));
        behaviours.add(new FlockCohesion(cohesionWeight,
                types.getName(Constants.LOCUST), minDistance,
                cohesionMaxDistance, maxAngle));
        behaviours.add(new FlockSeparation(separationWeight,
                types.getName(Constants.LOCUST),
                minDistance, seperationMaxDistance, maxAngle));
        behaviours.add(new Wander(wanderWeight));
        behaviours.add(new BoundryAvoid(boundryWeight, worldBounds, 100));
        behaviours.add(new SeekMaxEnergy(seekWeight, types.getName(
                Constants.FARM), seekMaxDistance));
        behaviours.add(new SeekMaxEnergy(seekWeight, types.getName(
                Constants.CROPS), seekMaxDistance /
                2));
        behaviours.add(new AvoidEntitiesFF(objectAvoidWeight, types.getName(
                Constants.WALL),
                objectAvoidForce));
        behaviours.add(new AvoidEntitiesFF(objectAvoidWeight, types.getName(
                Constants.MOUSE_DOWN),
                objectAvoidForce));
        behaviours.add(new AvoidEntitiesFF(objectAvoidWeight * 2, types.
                getName(Constants.MOUSE_MOVE),
                objectAvoidForce * 2));
        behaviours.add(new AvoidEntitiesFF(objectAvoidWeight * 2, types.
                getName(Constants.DEAD_LOCUST),
                objectAvoidForce * 2));
        return behaviours;
    }

    public static List<String> getEventKeys(Props props) {
        return props.getList("events");
    }

    public static List<EventTask> getEvents(Props props, Game game) {
        final List<String> keys = getEventKeys(props);

        final List<EventTask> tasks = new ArrayList<EventTask>();
        for (String key : keys) {
            tasks.add(getEvent(props, key, game));
        }
        return tasks;
    }

    public static EventTask getEvent(Props props, String name, Game game) {

        final String type = props.getProperty(name + ".type");
        final long delay = props.getLong(name + ".delay",
                EventManager.EventTask.DEFAULT_DELAY);
        final long period = props.getLong(name + ".period",
                EventManager.EventTask.DEFAULT_PERIOD);

        if (type.equalsIgnoreCase("SpawnEventTask")) {
            final String typeString = props.getProperty(name + ".entityType");
            final EntityType entityType = game.getEntityTypes().getName(
                    typeString);
            final int size = props.getInteger(name + ".size", 1);
            final int sizeVariance = props.getInteger(name + ".size-variance",
                    0);
            final String posName = props.getProperty(name + ".pos");
            final Position pos = getPosition(props, posName);
            return new EventManager.SpawnEventTask(delay, period, game, size,
                    sizeVariance, entityType, pos);
        } else {
            return null;
        }
    }

    public static Position getPosition(Props props, String name) {
        final String type = props.getProperty(name + ".type");
        if (type == null) {
            LOG.warn("No type given for position property: " + name);
            return null;
        }
        if (type.equalsIgnoreCase("ListedPosition")) {
            return getListedPosition(props, name);
        } else if (type.equalsIgnoreCase("VariablePosition")) {
            return getVariablePosition(props, name);
        } else if (type.equalsIgnoreCase("AbsolutePosition")) {
            return getAbsolutePosition(props, name);
        } else {
            LOG.warn("Unknown type for position property " + name);
            return null;
        }
    }

    public static ListedPosition getListedPosition(Props props, String name) {
        String prop = props.getProperty(name + ".order");
        int order;
        if (prop == null)
            order = ListedPosition.DEFAULT_ORDER;
        prop = prop.trim().toLowerCase();
        if (prop.matches("^asc.*"))
            order = ListedPosition.ORDER_ASCENDING;
        else if (prop.matches("^des.*"))
            order = ListedPosition.ORDER_DESCENDING;
        else if (prop.matches("^rand.*"))
            order = ListedPosition.ORDER_RANDOM;
        else
            order = ListedPosition.DEFAULT_ORDER;

        final ListedPosition result = new ListedPosition(order);

        int i = 0;
        String pos;
        while ((pos = props.getProperty(name + "[" + i + "]")) != null) {
            Position childPos = getPosition(props, pos);
            result.addSpawnPosition(childPos);
            i++;
        }
        return result;
    }

    public static VariablePosition getVariablePosition(Props props,
            String name) {
        final int x = props.getInteger(name + ".x");
        final int y = props.getInteger(name + ".y");
        final double o = props.getDouble(name + ".o");
        final int vx = props.getInteger(name + ".vx");
        final int vy = props.getInteger(name + ".vy");
        final double vo = props.getDouble(name + ".vo");
        return new VariablePosition(x, y, o, vx, vy, vo);
    }

    public static AbsolutePosition getAbsolutePosition(Props props,
            String name) {
        final int x = props.getInteger(name + ".x");
        final int y = props.getInteger(name + ".y");
        final double o = props.getDouble(name + ".o");
        return new AbsolutePosition(x, y, o);
    }
}
